package com.quantum.remoting.proxy
{
	import com.quantum.logging.LogFactory;

	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.utils.Proxy;
	import flash.utils.flash_proxy;

	import mx.core.Application;
	import mx.core.FlexGlobals;
	import mx.logging.ILogger;
	import mx.messaging.ChannelSet;
	import mx.messaging.channels.AMFChannel;
	import mx.messaging.channels.SecureAMFChannel;
	import mx.rpc.AbstractOperation;
	import mx.rpc.AsyncToken;
	import mx.rpc.IResponder;
	import mx.rpc.remoting.RemoteObject;
	import mx.utils.URLUtil;

	use namespace flash_proxy;

	dynamic public class RemoteObjectServiceProxy extends Proxy
	{
		private static var log:ILogger=LogFactory.getLogger(RemoteObjectServiceProxy);

		/** @private Default channel for all services. */
		private static var _defaultChannelUri:String=null;

		/* The default value of the flag indicating whether
		 to use a secured channel for service calls. */
		private static var _defaultSecure:Boolean=URLUtil.isHttpsURL(FlexGlobals.topLevelApplication.url);

		/** @private RemoteService instances. */
		private static var _services:Object={};

		/** @private Service destination to channel map. */
		private static var _registry:Object={};

		/** @private Indicates that the service registry Url is still loading. */
		private static var _registryLoadInProgress:Boolean=false;

		/** @private Service calls that are waiting to be processed. */
		private var _pendingOperations:Array=null;

		/** @private Object representing the actual remote service. */
		private var _service:RemoteObject=null;

		/** @private Determines if secured (i.e. SSL) channel needs to be used. */
		private var _secure:Boolean;

		/**
		 * @param secure Determines if secured (i.e. SSL) channel needs to be used.
		 * By default set to null, in which case application url is used to determine
		 * what channel to use. i.e. If application url contains https,
		 * uses secured amf channel else uses the default amf channel.
		 *
		 * @return The one and only instance of ExampleSingleton.
		 */
		public static function getInstance(destination:String, secure:Object=null):RemoteObjectServiceProxy
		{
			var service:RemoteObjectServiceProxy=_services[destination] as RemoteObjectServiceProxy;
			if (service == null)
			{
				service=new RemoteObjectServiceProxy(PrivateConstructorToken, destination, secure);
				_services[destination]=service;
			}

			return service;
		}

		/**
		 * @private Constructor. As a singleton, this class constructor prevents
		 * instantiation from other classes. Use RemoteService.getInstance()
		 * to work with the singleton from other classes.
		 *
		 * @throws Error if the constructor is called by an external class or if
		 *    the singelton has already been instantiated.
		 */
		public function RemoteObjectServiceProxy(token:Class, destination:String, secure:Object=null)
		{
			super();
			if (token != PrivateConstructorToken)
			{
				// Should this be localized? This error should be caught
				// during development. If not our testing has failed badly.
				throw new Error("RemoteService cannot be instantiated directly.");
			}

			_service=new RemoteObject(destination);
			_secure=(secure != null) ? Boolean(secure) : _defaultSecure;

			if (_defaultChannelUri != null)
			{
				this.channel=_defaultChannelUri;
				return;
			}

			if (_registryLoadInProgress)
			{
				_pendingOperations=new Array();
				return;
			}

			this.channel=_registry[_service.destination];
		}

		/**
		 * Channel to be used by this service.
		 *
		 * @param uri
		 *    The channel uri.
		 */
		public function set serviceChannelUri(uri:String):void
		{
			this.channel=uri;
		}

		/**
		 * Loads the service registry and extracts the channel
		 * information for all the registered services.
		 *
		 * @param url
		 *    Location to the service registry config xml.
		 */
		public static function set serviceRegistryUrl(url:String):void
		{
			_registryLoadInProgress=true;

			var loader:URLLoader=new URLLoader();
			loader.addEventListener(Event.COMPLETE, onUrlLoadSuccess);
			loader.addEventListener(IOErrorEvent.IO_ERROR, onUrlLoadFailure);
			loader.load(new URLRequest(url));
		}

		/**
		 * Default channel to be used by all the services running in
		 * the same remote application service.
		 *
		 * @param uri
		 *    The channel uri.
		 */
		public static function set defaultChannelUri(uri:String):void
		{
			_defaultChannelUri=uri;
		}

		/**
		 * Default for what kind of channel to be used for all
		 * services.
		 *
		 * If true means secured (i.e. SSL) channel will be used.
		 */
		public static function set defaultSecure(secure:Boolean):void
		{
			_defaultSecure=secure;
		}

		public static function get defaultSecure():Boolean
		{
			return _defaultSecure;
		}

		private static function onUrlLoadSuccess(event:Event):void
		{
			var loader:URLLoader=URLLoader(event.target);
			var services:XML=new XML(loader.data);
			for each (var serviceXml:XML in services.service)
			{
				_registry[serviceXml.@destination]=serviceXml.@channel;

				var service:RemoteObjectServiceProxy=_services[serviceXml.@destination];
				if (service != null)
				{
					service.channel=serviceXml.@channel;
					service.executePendingOperations();
				}
			}

			_registryLoadInProgress=false;
		}

		private static function onUrlLoadFailure(event:IOErrorEvent):void
		{
			// Since the service-registry loading failed there is no
			// need to re-attempt;hence setting the _registryLoadPending
			// to false. Lets use the supplied service destination
			// and the default channel.
			log.error("Service registry load failed: " + event.text);

			for each (var service:RemoteObjectServiceProxy in _services)
			{
				service.executePendingOperations();
			}

			_registryLoadInProgress=false;
		}

		override flash_proxy function callProperty(name:*, ... rest):*
		{
			try
			{
				// Queue operations until the service-registry loading is complete.
				if (_registryLoadInProgress)
				{
					log.debug("Loading service-registry.xml. Pushing operation: " + name + " into the pending queue.");

					// Return a temporary token so that the caller can add
					// IResponders instances. Once the service-registry is
					// loaded and processed, the responders will be copied back
					// into the AsyncToken returned by the actual service call.
					var asyncToken:AsyncToken=new AsyncToken(null);
					_pendingOperations.push(new PendingOperation(name, rest, asyncToken));
					return asyncToken;
				}

				return _service.getOperation(name).send.apply(this, rest);
			}
			catch (e:Error)
			{
				log.error("Remote service call failed: " + e.message);
			}
		}

		override flash_proxy function getProperty(name:*):*
		{
			return _service.getOperation(name);
		}

		private function set channel(uri:String):void
		{
			log.info("Using destination: " + _service.destination + " and channel: " + uri);

			if (uri == null || uri.length <= 0)
			{
				return;
			}

			var channelSet:ChannelSet=new ChannelSet();
			var amfChannel:AMFChannel;
			if (_secure)
			{
				amfChannel=new SecureAMFChannel("anyName", uri);
			}
			else
			{
				amfChannel=new AMFChannel("anyName", uri);
			}
			channelSet.addChannel(amfChannel);

			_service.channelSet=channelSet;
		}

		private function executePendingOperations():void
		{
			var length:int=_pendingOperations.length;
			for (var i:int=0; i < length; i++)
			{
				var operation:PendingOperation=PendingOperation(_pendingOperations[i]);
				var asyncToken:AsyncToken=_service.getOperation(operation.name).send.apply(this, operation.args);

				// Copy back IResponder instances from the cached AsyncToken into the
				// token returned by service call.
				for each (var responder:IResponder in operation.asyncToken.responders)
				{
					asyncToken.addResponder(responder);
				}

				// Copy back properties that were added to the AsyncToken at runtime.
				for (var property:String in operation.asyncToken)
				{
					asyncToken[property]=operation.asyncToken[property];
				}
			}

			_pendingOperations=[];
		}
	}
}

/////////////////////////////////////////////////////////////
//  Private
//

import mx.rpc.AbstractOperation;
import mx.rpc.AsyncToken;

class PendingOperation
{
	/** A reference to the method to be called. */
	public var name:String;

	/** The arguments to be passed to the method. */
	public var args:Array /* of Object */;

	public var asyncToken:AsyncToken;

	public function PendingOperation(name:String, args:Array=null, asyncToken:AsyncToken=null)
	{
		this.name=name;
		this.args=args;
		this.asyncToken=asyncToken;
	}
}

class PrivateConstructorToken
{
}
